/*
 * Copyright (c) SiteWhere, LLC. All rights reserved. http://www.sitewhere.com
 *
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */
package com.smartthing.mongodb.user;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bson.Document;

import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.MongoTimeoutException;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.model.IndexOptions;
import com.smartthing.core.SmartThingPersistence;
import com.smartthing.mongodb.IGlobalManagementMongoClient;
import com.smartthing.mongodb.MongoPersistence;
import com.smartthing.mongodb.common.MongoSiteWhereEntity;
import com.smartthing.rest.model.user.GrantedAuthority;
import com.smartthing.rest.model.user.GrantedAuthoritySearchCriteria;
import com.smartthing.rest.model.user.User;
import com.smartthing.server.lifecycle.LifecycleComponent;
import com.smartthing.spi.SmartThingException;
import com.smartthing.spi.SmartThingSystemException;
import com.smartthing.spi.error.ErrorCode;
import com.smartthing.spi.error.ErrorLevel;
import com.smartthing.spi.server.lifecycle.ILifecycleProgressMonitor;
import com.smartthing.spi.server.lifecycle.LifecycleComponentType;
import com.smartthing.spi.user.IGrantedAuthority;
import com.smartthing.spi.user.IGrantedAuthoritySearchCriteria;
import com.smartthing.spi.user.IUser;
import com.smartthing.spi.user.IUserManagement;
import com.smartthing.spi.user.IUserSearchCriteria;
import com.smartthing.spi.user.request.IGrantedAuthorityCreateRequest;
import com.smartthing.spi.user.request.IUserCreateRequest;

/**
 * User management implementation that uses MongoDB for persistence.
 * 
 * @author dadams
 */
public class MongoUserManagement extends LifecycleComponent implements IUserManagement {

    /** Static logger instance */
    private static Logger LOGGER = LogManager.getLogger();

    /** Injected with global SiteWhere Mongo client */
    private IGlobalManagementMongoClient mongoClient;

    public MongoUserManagement() {
	super(LifecycleComponentType.DataStore);
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.smartthing.server.lifecycle.LifecycleComponent#start(com.smartthing.spi
     * .server.lifecycle.ILifecycleProgressMonitor)
     */
    public void start(ILifecycleProgressMonitor monitor) throws SmartThingException {
	/** Ensure that expected indexes exist */
	ensureIndexes();
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.smartthing.spi.server.lifecycle.ILifecycleComponent#getLogger()
     */
    @Override
    public Logger getLogger() {
	return LOGGER;
    }

    /**
     * Ensure that expected collection indexes exist.
     * 
     * @throws SmartThingException
     */
    protected void ensureIndexes() throws SmartThingException {
	getMongoClient().getUsersCollection().createIndex(new Document("username", 1), new IndexOptions().unique(true));
	getMongoClient().getAuthoritiesCollection().createIndex(new Document("authority", 1),
		new IndexOptions().unique(true));
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.smartthing.spi.user.IUserManagement#createUser(com.smartthing.spi.user.
     * request.IUserCreateRequest, boolean)
     */
    public IUser createUser(IUserCreateRequest request, boolean encodePassword) throws SmartThingException {
	User user = SmartThingPersistence.userCreateLogic(request, encodePassword);

	MongoCollection<Document> users = getMongoClient().getUsersCollection();
	Document created = MongoUser.toDocument(user);
	MongoPersistence.insert(users, created, ErrorCode.DuplicateUser);
	return user;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.smartthing.spi.user.IUserManagement#importUser(com.smartthing.spi.user.
     * IUser, boolean)
     */
    @Override
    public IUser importUser(IUser imported, boolean overwrite) throws SmartThingException {
	IUser existing = getUserByUsername(imported.getUsername());
	if (existing != null) {
	    if (!overwrite) {
		throw new SmartThingSystemException(ErrorCode.DuplicateUser, ErrorLevel.ERROR,
			HttpServletResponse.SC_CONFLICT);
	    }
	    deleteUser(imported.getUsername(), true);
	}
	User user = User.copy(imported);

	MongoCollection<Document> users = getMongoClient().getUsersCollection();
	Document created = MongoUser.toDocument(user);
	MongoPersistence.insert(users, created, ErrorCode.DuplicateUser);
	return user;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.smartthing.spi.user.IUserManagement#authenticate(java.lang.String,
     * java.lang.String, boolean)
     */
    @Override
    public IUser authenticate(String username, String password, boolean updateLastLogin) throws SmartThingException {
	if (password == null) {
	    throw new SmartThingSystemException(ErrorCode.InvalidPassword, ErrorLevel.ERROR,
		    HttpServletResponse.SC_BAD_REQUEST);
	}
	Document userObj = assertUser(username);
	String inPassword = SmartThingPersistence.encodePassword(password);
	User match = MongoUser.fromDocument(userObj);
	if (!match.getHashedPassword().equals(inPassword)) {
	    throw new SmartThingSystemException(ErrorCode.InvalidPassword, ErrorLevel.ERROR,
		    HttpServletResponse.SC_UNAUTHORIZED);
	}

	// Update last login date if requested.
	if (updateLastLogin) {
	    match.setLastLogin(new Date());
	    Document updated = MongoUser.toDocument(match);
	    MongoCollection<Document> users = getMongoClient().getUsersCollection();
	    Document query = new Document(MongoUser.PROP_USERNAME, username);
	    MongoPersistence.update(users, query, updated);
	}

	return match;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.smartthing.spi.user.IUserManagement#updateUser(java.lang.String,
     * com.smartthing.spi.user.request.IUserCreateRequest, boolean)
     */
    @Override
    public IUser updateUser(String username, IUserCreateRequest request, boolean encodePassword)
	    throws SmartThingException {
	Document existing = assertUser(username);

	// Copy any non-null fields.
	User updatedUser = MongoUser.fromDocument(existing);
	SmartThingPersistence.userUpdateLogic(request, updatedUser, encodePassword);

	Document updated = MongoUser.toDocument(updatedUser);

	MongoCollection<Document> users = getMongoClient().getUsersCollection();
	Document query = new Document(MongoUser.PROP_USERNAME, username);
	MongoPersistence.update(users, query, updated);
	return MongoUser.fromDocument(updated);
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.smartthing.spi.user.IUserManagement#getUserByUsername(java.lang.
     * String)
     */
    @Override
    public IUser getUserByUsername(String username) throws SmartThingException {
	Document dbUser = getUserDocumentByUsername(username);
	if (dbUser != null) {
	    return MongoUser.fromDocument(dbUser);
	}
	return null;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.smartthing.spi.user.IUserManagement#getGrantedAuthorities(java.lang.
     * String)
     */
    @Override
    public List<IGrantedAuthority> getGrantedAuthorities(String username) throws SmartThingException {
	IUser user = getUserByUsername(username);
	List<String> userAuths = user.getAuthorities();
	List<IGrantedAuthority> all = listGrantedAuthorities(new GrantedAuthoritySearchCriteria());
	List<IGrantedAuthority> matched = new ArrayList<IGrantedAuthority>();
	for (IGrantedAuthority auth : all) {
	    if (userAuths.contains(auth.getAuthority())) {
		matched.add(auth);
	    }
	}
	return matched;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.smartthing.spi.user.IUserManagement#addGrantedAuthorities(java.lang.
     * String, java.util.List)
     */
    @Override
    public List<IGrantedAuthority> addGrantedAuthorities(String username, List<String> authorities)
	    throws SmartThingException {
	throw new SmartThingException("Not implemented.");
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.smartthing.spi.user.IUserManagement#removeGrantedAuthorities(java.lang
     * .String, java.util.List)
     */
    @Override
    public List<IGrantedAuthority> removeGrantedAuthorities(String username, List<String> authorities)
	    throws SmartThingException {
	throw new SmartThingException("Not implemented.");
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.smartthing.spi.user.IUserManagement#listUsers(com.smartthing.spi.user.
     * request .IUserSearchCriteria)
     */
    @Override
    public List<IUser> listUsers(IUserSearchCriteria criteria) throws SmartThingException {
	try {
	    MongoCollection<Document> users = getMongoClient().getUsersCollection();
	    Document dbCriteria = new Document();
	    if (!criteria.isIncludeDeleted()) {
		MongoSiteWhereEntity.setDeleted(dbCriteria, false);
	    }
	    FindIterable<Document> found = users.find(dbCriteria).sort(new BasicDBObject(MongoUser.PROP_USERNAME, 1));
	    MongoCursor<Document> cursor = found.iterator();

	    List<IUser> matches = new ArrayList<IUser>();
	    try {
		while (cursor.hasNext()) {
		    Document match = cursor.next();
		    matches.add(MongoUser.fromDocument(match));
		}
	    } finally {
		cursor.close();
	    }
	    return matches;
	} catch (MongoTimeoutException e) {
	    throw new SmartThingException("Connection to MongoDB lost.", e);
	}
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.smartthing.spi.user.IUserManagement#deleteUser(java.lang.String,
     * boolean)
     */
    @Override
    public IUser deleteUser(String username, boolean force) throws SmartThingException {
	Document existing = assertUser(username);
	if (force) {
	    MongoCollection<Document> users = getMongoClient().getUsersCollection();
	    MongoPersistence.delete(users, existing);
	    SmartThingPersistence.userDeleteLogic(username);
	    return MongoUser.fromDocument(existing);
	} else {
	    MongoSiteWhereEntity.setDeleted(existing, true);
	    Document query = new Document(MongoUser.PROP_USERNAME, username);
	    MongoCollection<Document> users = getMongoClient().getUsersCollection();
	    MongoPersistence.update(users, query, existing);
	    return MongoUser.fromDocument(existing);
	}
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.smartthing.spi.user.IUserManagement#createGrantedAuthority(com.
     * sitewhere.spi .user.request. IGrantedAuthorityCreateRequest)
     */
    @Override
    public IGrantedAuthority createGrantedAuthority(IGrantedAuthorityCreateRequest request) throws SmartThingException {
	GrantedAuthority auth = SmartThingPersistence.grantedAuthorityCreateLogic(request);
	MongoCollection<Document> auths = getMongoClient().getAuthoritiesCollection();
	Document created = MongoGrantedAuthority.toDocument(auth);
	MongoPersistence.insert(auths, created, ErrorCode.DuplicateAuthority);
	return auth;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.smartthing.spi.user.IUserManagement#getGrantedAuthorityByName(java.
     * lang.String)
     */
    @Override
    public IGrantedAuthority getGrantedAuthorityByName(String name) throws SmartThingException {
	Document dbAuth = getGrantedAuthorityDocumentByName(name);
	if (dbAuth != null) {
	    return MongoGrantedAuthority.fromDBObject(dbAuth);
	}
	return null;
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.smartthing.spi.user.IUserManagement#updateGrantedAuthority(java.lang.
     * String, com.smartthing.spi.user.request.IGrantedAuthorityCreateRequest)
     */
    @Override
    public IGrantedAuthority updateGrantedAuthority(String name, IGrantedAuthorityCreateRequest request)
	    throws SmartThingException {
	throw new SmartThingException("Not implemented.");
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.smartthing.spi.user.IUserManagement#listGrantedAuthorities(com.
     * sitewhere.spi .user. IGrantedAuthoritySearchCriteria)
     */
    @Override
    public List<IGrantedAuthority> listGrantedAuthorities(IGrantedAuthoritySearchCriteria criteria)
	    throws SmartThingException {
	try {
	    MongoCollection<Document> auths = getMongoClient().getAuthoritiesCollection();
	    FindIterable<Document> found = auths.find()
		    .sort(new BasicDBObject(MongoGrantedAuthority.PROP_AUTHORITY, 1));
	    MongoCursor<Document> cursor = found.iterator();

	    List<IGrantedAuthority> matches = new ArrayList<IGrantedAuthority>();
	    try {
		while (cursor.hasNext()) {
		    Document match = cursor.next();
		    matches.add(MongoGrantedAuthority.fromDBObject(match));
		}
	    } finally {
		cursor.close();
	    }
	    return matches;
	} catch (MongoTimeoutException e) {
	    throw new SmartThingException("Connection to MongoDB lost.", e);
	}
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * com.smartthing.spi.user.IUserManagement#deleteGrantedAuthority(java.lang.
     * String)
     */
    @Override
    public void deleteGrantedAuthority(String authority) throws SmartThingException {
	throw new SmartThingException("Not implemented.");
    }

    /**
     * Get the {@link DBObject} for a User given username. Throw an exception if
     * not found.
     * 
     * @param username
     * @return
     * @throws SmartThingException
     */
    protected Document assertUser(String username) throws SmartThingException {
	Document match = getUserDocumentByUsername(username);
	if (match == null) {
	    throw new SmartThingSystemException(ErrorCode.InvalidUsername, ErrorLevel.ERROR,
		    HttpServletResponse.SC_NOT_FOUND);
	}
	return match;
    }

    /**
     * Get the {@link Document} for a User given unique username.
     * 
     * @param username
     * @return
     * @throws SmartThingException
     */
    protected Document getUserDocumentByUsername(String username) throws SmartThingException {
	try {
	    MongoCollection<Document> users = getMongoClient().getUsersCollection();
	    Document query = new Document(MongoUser.PROP_USERNAME, username);
	    return users.find(query).first();
	} catch (MongoTimeoutException e) {
	    throw new SmartThingException("Connection to MongoDB lost.", e);
	}
    }

    /**
     * Get the {@link Document} for a GrantedAuthority given name. Throw an
     * exception if not found.
     * 
     * @param name
     * @return
     * @throws SmartThingException
     */
    protected Document assertGrantedAuthority(String name) throws SmartThingException {
	Document match = getGrantedAuthorityDocumentByName(name);
	if (match == null) {
	    throw new SmartThingSystemException(ErrorCode.InvalidAuthority, ErrorLevel.ERROR,
		    HttpServletResponse.SC_NOT_FOUND);
	}
	return match;
    }

    /**
     * Get the {@link Document} for a GrantedAuthority given unique name.
     * 
     * @param name
     * @return
     * @throws SmartThingException
     */
    protected Document getGrantedAuthorityDocumentByName(String name) throws SmartThingException {
	try {
	    MongoCollection<Document> auths = getMongoClient().getAuthoritiesCollection();
	    Document query = new Document(MongoGrantedAuthority.PROP_AUTHORITY, name);
	    return auths.find(query).first();
	} catch (MongoTimeoutException e) {
	    throw new SmartThingException("Connection to MongoDB lost.", e);
	}
    }

    public IGlobalManagementMongoClient getMongoClient() {
	return mongoClient;
    }

    public void setMongoClient(IGlobalManagementMongoClient mongoClient) {
	this.mongoClient = mongoClient;
    }
}